%%%-------------------------------------------------------------------
%%% @author liuwentao
%%% @doc
%%%
%%% @end
%%% Created : 24. 5月 2021 14:14
%%%-------------------------------------------------------------------
-module(make_code).

-behaviour(gen_server).

-export([
    up/0
    , up/1
    , up/2

]).

-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-define(SERVER, ?MODULE).
-include("common.hrl").

-record(code_state, {}).


%% @doc 热更新
up() ->
    O = ets:tab2list(make_code),
    N = beam_hash(),
    %% 先移除旧模块，加载新模块
    do_up(N, O, [], fun code:soft_purge/1).


-spec up(Option) -> list() when
    Option :: force | [ModName],
    ModName :: atom().
up(force) ->
    N = beam_hash(),
    O = ets:tab2list(make_code),
    %% 清除一个模块代码
    do_up(N, O, [], fun code:purge/1);
up(ModList) ->
    O = ets:tab2list(make_code),
    N = do_beam_hash(ModList, []),
    do_up(N, O, [], fun code:soft_purge/1).

%% @doc 重新检查并加载指定的模块，并强制清除运行中的旧代码
-spec up([ModName], force) -> list() when
    ModName :: atom().
up(ModList, force) ->
    O = ets:tab2list(make_code),
    N = do_beam_hash(ModList, []),
    do_up(N, O, [], fun code:purge/1).



%%%===================================================================
%%% Spawning and gen_server implementation
%%%===================================================================

start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).


init([]) ->
    ets:new(make_code, [named_table, public, set]),
    %% 获取所有加载的模块hash值 放入ets中 做热更新
    [ets:insert(make_code, X) || X <- beam_hash()],
    %% 加载所有模块
    load_all_file(),
    State = #code_state{},
    {ok, State}.

handle_call(_Request, _From, State = #code_state{}) ->
    {reply, ok, State}.

handle_cast(_Request, State = #code_state{}) ->
    {noreply, State}.

handle_info(_Info, State = #code_state{}) ->
    {noreply, State}.

terminate(_Reason, _State = #code_state{}) ->
    ok.

code_change(_OldVsn, State = #code_state{}, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
%% @doc 获取所有的beam文件的hash值
beam_hash() ->
    do_beam_hash(beam_file(), []).

%% @doc 返回beam hash
do_beam_hash([], List) -> List;
do_beam_hash([N | T], List) ->
    L = case beam_lib:md5(?code_path ++ "/ebin/" ++ N) of
            {ok, {M, Md5}} ->
                [{M, util:md5(Md5)} | List];
            Err ->
                ?ERR("无法获取文件[~s]的hash值:~w", [N, Err]),
                List
        end,
    do_beam_hash(T, L).


%% @doc 获取beam文件列表
-spec beam_file() -> list().
beam_file() ->
    case file:list_dir(?code_path ++ "/ebin") of
        {ok, FList} ->
            do_beam_file(FList, []);
        {error, Why} ->
            ?ERR("获取beam文件时发生异常(~ts): ~w", [?code_path, Why]),
            []
    end.


%% @doc 获取所有的beam文件
do_beam_file([], List) -> List;
do_beam_file([F | T], List) ->
    L = case filename:extension(F) =:= ".beam" of
            true ->
                M = filename:basename(filename:rootname(F)),
                [M | List];
            _ -> List
        end,
    do_beam_file(T, L).


%% @doc 执行更新
do_up([], _O, Rtn, _Fun) -> Rtn;
do_up([{Mod, NewHash} | N], O, Rtn, Fun) ->
    NewRtn =
        case lists:keyfind(Mod, 1, O) of
            false ->
                %% 找不到就文件，更新
                [load_beam(Mod, NewHash, Fun) | Rtn];
            {_, OldHash} ->
                %% 旧文件和新文件hash值一样，不更新
                case OldHash =:= NewHash of
                    true -> Rtn;
                    false -> [load_beam(Mod, NewHash, Fun) | Rtn]
                end
        end,
    do_up(N, O, NewRtn, Fun).


%% @doc 加载beam文件(热更新)
load_beam(Mod, Hash, PurgeFun) ->
    PurgeFun(Mod),
    %% 加载模块
    case code:load_file(Mod) of
        {module, _} ->
            ets:insert(make_code, {Mod, Hash}),
            {Mod, ok};
        {error, Why} ->
            {Mod, {error, Why}}
    end.


%% @doc 加载所有模块
load_all_file() ->
    FirstKey = ets:first(make_code),
    load_all_file(FirstKey).

%% @doc 加载模块
load_all_file('$end_of_table') ->
    ok;
load_all_file(Mod) ->
    code:load_file(Mod),
    NextMod = ets:next(make_code, Mod),
    load_all_file(NextMod).

